package com.atguigu.tingshu.common.auth.login.aspect;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.auth.login.annotation.NeedLogin;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Map;

@Aspect
@Component
public class LoginAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;


    // 此处有多切面失效的问题
    // 如一个这个注解标记的类上 有事物注解 那么如果此方法抛出异常 在此切面却catch 那么异常被吃掉了 事物不会回滚
    @Around(value = "@annotation(com.atguigu.tingshu.common.auth.login.annotation.NeedLogin)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        // 判断用户是否登录
        // 获取请求头中的token 是前端携带的
        Object result;
        String token = getHeaderToken();
        // 先判断 注解中的required属性 如果是false 则说明可以不需要登录
        Boolean isLogin = getMethodAnnotationLoginValue(joinPoint);
        if (!isLogin) {
            // 如果不需要登录 直接执行目标方法(但先看看token带没带)
            if(!StringUtils.hasText(token)){
                return joinPoint.proceed();
            }
        }

        if (!StringUtils.hasText(token)) {
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);// 未登录
        }

        Long userId = checkTokenAndGetUserId(token);

        // 将userId放入ThreadLocal中 因为切面和目标方法在同一个线程中 这种方式可以方便的获取到userId
        AuthContextHolder.setUserId(userId);

        try {
            result = joinPoint.proceed();
        } finally {
            AuthContextHolder.removeUserId();
        }

        return result;
    }

    private Boolean getMethodAnnotationLoginValue(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        NeedLogin annotation = method.getAnnotation(NeedLogin.class);
        return annotation.required();
    }

    private Long checkTokenAndGetUserId(String token) {
        // 校验一下JWT是否被篡改 若是被篡改会抛出异常
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(PublicConstant.PUBLIC_KEY));

        String claims = jwt.getClaims();
        Map map = JSONObject.parseObject(claims, Map.class);
        Object userId = map.get("userId");
        String openId = (String) map.get("openId");

        // 判断redis中是否有token
        String tokenInRedis = redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX + openId);
        if (!StringUtils.hasText(tokenInRedis) || !token.equals(tokenInRedis)) {
            throw new GuiguException(401, "accessToken已过期！");
        }

        return Long.parseLong(userId.toString());
    }

    private static String getHeaderToken() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = null;
        if (requestAttributes != null) {
            request = requestAttributes.getRequest();
        }else {
            throw new GuiguException(201, "请求头中缺少token信息");
        }
        String token = request.getHeader("token");
        return token;
    }
}
